package org.ctstudio.sei;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class SimpleExpressionInterpreter {
  protected String expressionStart = "${";
  protected String expressionEnd = "}";
  protected String invocationSplit = "|";
  protected String methodNameSplit = ":";
  protected String parameterSplit = ",";
  protected char escape = '\\';

  private final Object methodProvider;

  public SimpleExpressionInterpreter(Object methodProvider) {
    this.methodProvider = methodProvider;
  }

  public String eval(String template) {
    if (template == null || template.length() == 0) return template;
    String result = template;
    List<String> expressions = findExpressions(template);
    for (String expression : expressions) {
      String value = evalExpression(expression);
      result = result.replace(expression, value);
    }
    return result;
  }

  int nextDivider(String template, String divider, int fromIndex) {
    int pos;
    int from = fromIndex;
    do {
      pos = template.indexOf(divider, from);
      if (pos == 0) return pos;
      if (pos > 0 && template.charAt(pos - 1) != escape) return pos;
      from = pos + 1;
    } while (pos >= 0);
    return -1;
  }

  List<String> findExpressions(String template) {
    List<String> expressions = new LinkedList<>();
    int fromIndex = 0;
    String expression;
    do {
      int beginIndex = nextDivider(template, expressionStart, fromIndex);
      if (beginIndex < 0) break;
      int endIndex = nextDivider(template, expressionEnd, beginIndex + expressionStart.length());
      if (endIndex < 0) break;
      expression = template.substring(beginIndex, endIndex + expressionEnd.length());
      expressions.add(expression);
      fromIndex = endIndex + expressionEnd.length();
    } while (true);
    return expressions;
  }

  List<Invocation> parseInvocations(String expression) {
    if (expression == null || expression.length() < expressionStart.length() + expressionEnd.length()) return Collections.emptyList();
    String statement = expression.substring(expressionStart.length(), expression.length() - expressionEnd.length());
    String[] phrases = split(statement, invocationSplit);
    List<Invocation> invocations = new ArrayList<>(phrases.length);
    for (String phrase : phrases) {
      invocations.add(parseInvocation(phrase));
    }
    return invocations;
  }

  private Invocation parseInvocation(String phrase) {
    int methodNameEndIndex = phrase.indexOf(methodNameSplit);
    if (methodNameEndIndex > 0) {
      String method = phrase.substring(0, methodNameEndIndex);
      String parameterStr = phrase.substring(methodNameEndIndex + methodNameSplit.length());
      String[] parameters = parameterStr.split(parameterSplit);
      return new Invocation(method, parameters);
    } else {
      return new Invocation(phrase);
    }
  }

  String[] split(String text, String delimiter) {
    if (text == null) return null;
    if (delimiter == null || delimiter.length() == 0) return new String[]{text};
    List<String> data = new ArrayList<>();
    int pos = 0;
    for (int from = 0; from >= 0; from = pos + delimiter.length()) {
      pos = text.indexOf(delimiter, from);
      if (pos >= 0) {
        data.add(text.substring(from, pos));
      } else {
        data.add(text.substring(from));
        break;
      }
    }
    return data.toArray(new String[0]);
  }

  private Method findMethod(Class<?> clazz, String methodName) {
    for (Method m : clazz.getMethods()) {
      if (m.getName().equals(methodName)) {
        return m;
      }
    }
    return null;
  }

  private Object evalInvocations(List<Invocation> invocations) {
    boolean firstCall = true;
    Object result = null;
    try {
      for (Invocation invocation : invocations) {
        Method m = findMethod(methodProvider.getClass(), invocation.getMethod());
        if (m == null) return null;
        Object[] args;
        if (invocation.getExtraParams() != null) {
          args = new Object[invocation.getExtraParams().length + (firstCall ? 0 : 1)];
          if (!firstCall) args[0] = result;
          System.arraycopy(invocation.getExtraParams(), 0, args, firstCall ? 0 : 1, invocation.getExtraParams().length);
        } else {
          args = firstCall ? new Object[0] : new Object[]{result};
        }
        result = m.invoke(methodProvider, args);
        firstCall = false;
      }
    } catch (IllegalAccessException | InvocationTargetException e) {
      return null;
    }
    return result;
  }

  String evalExpression(String expression) {
    List<Invocation> invocations = parseInvocations(expression);
    Object result = evalInvocations(invocations);
    return result == null ? "" : result.toString();
  }
}
